Skip to content

docs: scope client branding (product name + logo to id.ai), session-bound contract - #103

Open
aterga wants to merge 3 commits into
mainfrom
claude/client-branding-proposal
Open

aterga wants to merge 3 commits into
mainfrom
claude/client-branding-proposal

Conversation

@aterga

@aterga aterga commented Jul 29, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

A scoping doc, docs/scoping-client-branding.md, for surfacing the MCP client's product identity to Internet Identity. The id.ai consent screen can then show which vetted product will receive the access a connect grants: a name and a logo. This PR is docs-only. The doc is the boundary contract for both sides; the server side is implemented in #200.

Revised (2026-09-24). The first draft had the server put the product slug in the connect-link fragment (&connector=<slug>) and serve an unbound GET {issuer}/branding/{slug}. Whoever drives the browser writes the fragment, so that let a consent screen vouch for a product the server never tied to the connect. The contract now binds branding to the connect itself:

authorize:  MCP server --302 to id.ai/mcp#callback=…&state=…&ttl=…&registration_key=…-->  id.ai   (link unchanged)
lookup:     id.ai --GET {issuer}/branding?state=<state>-->  MCP server
            MCP server --200 {name, logo, verified} | 404, no-store-->  id.ai
logo:       id.ai --<img src="{issuer}/branding/<slug>/logo">-->  MCP server (static SVG)
consent:    id.ai shows the name + logo only if the callback origin is on II's own trusted list

Key points

  • Session-bound. The server answers from the connect's validated redirect_uri, recorded at authorize. It never trusts anything the link or the client asserts.
    • Loopback (native-app) connects get 404, as do unlisted hosts, operator-added allow-list entries, and unknown or expired states. All these 404s are identical.
  • What branding vouches for. The recorded redirect names where the authorization code is delivered. It does not name who started the connect, or which account at that vendor ends up holding the grant. The doc works through the residual risks that follow: a leaked code, a hostile local app reading browser history, and a connect started from an attacker's own vendor account.
  • II requirements (6.2):
    1. derive {issuer} from the #4091-validated callback with a strict parsed check;
    2. accept only a well-formed 200 and treat everything else as "no branding";
    3. use one parsed callback and one state for the #4091 check, the lookup, and the return navigation;
    4. gate the whole treatment (name, logo, and badge) on an exact-origin trust list kept by II, since #4091 proves only that an origin declared the callback;
    5. render safely: plain-text name, <img>-only logo from the callback's origin, CSP, wording about where the access goes;
    6. keep the connector distinct from the server's own origin and app name.
  • Vetting. Only compiled-in callbacks (DEFAULT_ALLOWED_REDIRECTS) are branded. Nothing keys on client_id (DCR ids churn; CIMD ids are the client's own claim). v1 covers redirect-identified web connectors; native apps stay anonymous until a signed software_statement path exists.
  • Accuracy. Code is cited by symbol, not line number. The doc was adversarially checked against Surface vetted-connector branding to Internet Identity, bound to the authorization session #200's code in two passes before this push.

Related

Testing

Docs-only; there is no build or test impact. The server-side behavior it describes is covered by #200's tests, listed in section 9 of the doc.

🤖 Generated with Claude Code

A scoping doc for surfacing the requesting MCP client's product identity to
Internet Identity, so the id.ai consent screen can show which vetted product
is connecting (a name and a logo) instead of an anonymous bridge.

The design:
- The MCP server puts a curated product-name slug in the II connect-link
  fragment, and serves the matching name and logo from two GET endpoints on
  the same origin II already validates for the #4091 callback check.
- Branding is shown for vetted products only; the name and logo are curated
  server-side and never taken from client-supplied DCR metadata (open DCR is
  unauthenticated, so rendering client strings would be a consent-phishing
  vector).
- Nothing keys on the OAuth client_id: those are per-registration UUIDs that
  churn on reinstall, expiry, and LRU eviction. Product identity is derived
  from the vetted redirect vendor instead (the hosted-redirect allow-list),
  which is stable and self-authenticating.
- v1 covers self-authenticating web connectors only. Native/desktop apps
  authenticate over loopback (no verifiable identity), so they stay anonymous
  until a signed software_statement path lands (deferred).

Docs-only; requires a coordinated counterpart change on the id.ai side, which
the doc specifies as the boundary contract.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014T9N8USDfNK5yzznPGg7Ym
@aterga
aterga requested a balanced review from Copilot July 29, 2026 17:06

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a scoping/design document defining a proposed “client branding” extension for MCP→Internet Identity authorization, allowing id.ai to show a vetted connector’s product name and logo on the consent screen by using a curated server-side product slug and same-origin endpoints.

Changes:

  • Add docs/scoping-client-branding.md describing the proposed contract/flow, vetting model, endpoints, security considerations, and phased rollout plan.
Comments suppressed due to low confidence (4)

docs/scoping-client-branding.md:55

  • The POST /oauth/register citation is off: the handler is register at src/auth.rs:1897 (the current 1888 line is inside granted_grant_types). Please update the reference to match the actual endpoint implementation.
Dynamic client registration is **unauthenticated and open** (`POST /oauth/register` takes all
callers, `auth.rs:1888`). So any client-supplied `client_name` / `logo_uri` is attacker-
controlled. Rendering it on a consent screen would be a phishing gift: a hostile client

docs/scoping-client-branding.md:70

  • The include_str! reference for the inlined DFINITY logo points to auth.rs:1161, but the logo include is currently CONNECT_LOGO_SVG at src/auth.rs:1170 (1161 is in the preceding doc/comment block). Updating the citation will keep the “verified” claim accurate.
  client `logo_uri`. Logos are compiled-in assets (like the DFINITY logo already inlined into the
  callback page via `include_str!`, `auth.rs:1161`).

docs/scoping-client-branding.md:77

  • The client_id minting citation appears to be pointing at the invalid-redirect error branch (src/auth.rs:1913), not where the client-{uuid_v4} value is created (src/auth.rs:1922). Updating the line reference will prevent confusion when someone goes to verify the claim.
- Minted fresh per registration: `client-{uuid_v4}` (`auth.rs:1913`), a new random value each
  time anyone registers.

docs/scoping-client-branding.md:185

  • This section says the /branding responses are “cacheable”, but earlier the metadata endpoint is specified as no-store (and later you reiterate no-store for the JSON). Consider clarifying that only the logo bytes are intended to be cacheable, while the JSON metadata should be no-store (or adjust the endpoint contract if you intend caching for both).
- **Bounded, closed slug space.** Both GETs serve only from the fixed curated set; an unknown slug
  is a `404`. Responses are small, cacheable, and served from compiled-in assets.
- **Logo rendering on II (the one II-side must).** SVG is acceptable as the logo format, but only
  if II renders it through a fixed-size `<img src=...>`. It must never inline server-returned SVG
  into the consent DOM: inlined SVG can execute script, an `<img>`-loaded SVG cannot. (A raster
  logo would sidestep the concern entirely, but `<img>`-rendered SVG is safe and keeps the crisp
  vector mark.)
- **CORS / caching** mirror the `#4091` well-known: `Access-Control-Allow-Origin` for the JSON
  metadata (II uses `fetch`), and `no-store` so an intermediary cannot serve a mapping stale after
  a curation change.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread docs/scoping-client-branding.md Outdated
Copilot noted the "verified, file:line" citations had drifted from
src/auth.rs. The doc's numbers were taken from the pre-merge #98 branch;
after #98 (the MCP05 redirect fix) merged, everything below
redirect_uri_permitted shifted by about nine lines. Re-synced every
citation against current main and verified each against the source:

  authorize            865        -> 874
  validate_client      890 (call) -> 759 (definition)
  #4091 well-known     1082-1109  -> 1091-1118
  error copy           923        -> 932
  ii_mcp_url           1062       -> 1071
  connect_callback_url 1088       -> 1097
  auth_callbacks       1097       -> 1106
  logo include_str!    1161       -> 1170
  register             1888       -> 1897
  client-{uuid}        1913       -> 1922

Unchanged (above the shift): make_room_for_client (340),
DEFAULT_ALLOWED_REDIRECTS (399), the path-pin comment (383-391), and the
loopback-exempt note (393).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014T9N8USDfNK5yzznPGg7Ym
@aterga
aterga marked this pull request as ready for review July 30, 2026 11:54
@aterga
aterga requested review from a team and Copilot July 30, 2026 11:54

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (2)

docs/scoping-client-branding.md:20

  • This section says “Two decisions frame everything below,” but it lists three bullets. Update the count to match to avoid confusion in the scoping doc.
Two decisions frame everything below:

docs/scoping-client-branding.md:177

  • This bullet says responses are “cacheable,” but a few lines later the doc specifies no-store for the JSON metadata. Clarify which endpoint(s) are intended to be cacheable vs explicitly non-cacheable so implementers don’t pick conflicting headers.
- **Bounded, closed slug space.** Both GETs serve only from the fixed curated set; an unknown slug
  is a `404`. Responses are small, cacheable, and served from compiled-in assets.

@aterga aterga left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review from the II-side design point of view

Answering §10, since it's addressed to the II team. Grounded in the current II frontend/backend (the id.ai/mcp connect flow, the declared-callback allow-list, and the production CSP).

The identity/trust model is right — no changes needed there

  • Curated, server-side product identity, never client-supplied. Correct. II isn't a party to the client↔server OAuth, so anything the client can set (DCR client_name, logo_uri) is unauthenticated and phishable. Deriving identity from the vetted redirect vendor is a sound proxy: II's declared-callback allow-list (fetch of /.well-known/ii-auth-callbacks, fail-closed, exact match) already means a connect can only reach us over an origin that published that manifest, so the vendor is effectively attested by the same mechanism.
  • Product slug, not client_id. Correct. The connect link is attacker-craftable and client-{uuid_v4} churns / is LRU-evicted, so a client_id in the fragment would be a spoofable, unstable display key. A closed slug space resolved server-side is the right identifier.
  • Additive / backward-compatible contract. Confirmed on our end — the link parser ignores unknown fragment params today, so an added connector is safe for old II, and old servers simply omit it. The only II code cost is one field added to the link schema.

The one real gap: logo transport will hit II's CSP (§10 Q2)

The doc's model — II renders '&lt;img src="{issuer}/branding/{slug}/logo"&gt;' pointing at the callback origin — is blocked by II's Content-Security-Policy. This is the main thing to revise.

II's production CSP is:

img-src 'self' data: https://*.googleusercontent.com;
connect-src 'self' https:;

So:

  • A cross-origin '&lt;img src="https://&lt;callback-origin&gt;/…/logo"&gt;' won't load — img-src allows only 'self', data:, and one fixed Google-avatar host. We don't want to widen img-src to https:, since that reopens exactly the cross-origin image surface we keep closed.
  • fetch() to any HTTPS origin is allowed (connect-src 'self' https:).

Recommended contract instead: II fetch()es the branding from the trust-confirmed callback origin and renders the logo as a data: URI (already permitted by img-src data:, no CSP change). Simplest shape: serve one metadata document with the logo inlined as a bounded base64 data: image, so II makes a single request to a single origin and never needs a second cross-origin image hop. That also answers §10 Q4 — one doc with an inline logo beats separate name/logo requests for us.

Note this flips one assumption in the doc: "No CORS needed for <img>" doesn't apply, because we reach the logo via fetch() (which does need the CORS header you already specify) and then render data:.

SVG vs raster (§10 Q2 cont.)

The doc's point that an <img>-loaded SVG can't execute script is technically true. But once II is building a data: URI from fetched bytes, I'd steer away from SVG: it keeps a latent "never inline this" invariant that future contributors won't know, whereas raster (PNG) is strictly safer and removes the question entirely. Recommend the curated logos be PNG, with II applying a raster-only content-type allow-list, a byte-size cap, a decode step, and a fail-closed fallback to the anonymous icon on any validation failure — i.e. apply the same hardening the doc reserves for a hypothetical Phase-3 open path, in v1, because II is the side putting bytes on screen.

Trust ordering — please state it explicitly

connector={slug} lives in the fragment, which is attacker-craftable (a malicious link can claim &connector=claude). Safety comes entirely from where II fetches branding: only from the callback origin, and only after that origin is trust-confirmed — i.e. after the certified trusted_url gate confirms the server origin, and after the callback is matched against the origin's declared allow-list. The slug is display-only routing; the authority for "is this really ChatGPT" is that the branding came from an origin II independently trust-confirmed, never the fragment's claimed value. Worth spelling out so the II implementation doesn't fetch branding on the honest-untrusted / unverified path.

§10, point by point

  1. Fragment param + endpoint paths — connector={slug} is fine (additive; parser ignores unknowns). Prefer a single metadata endpoint with an inline logo over a separate /logo route.
  2. Render logo via fixed-size '&lt;img src=…&gt;'? Not cross-origin (CSP blocks it). Yes via fetch() + data:, or an inline base64 data: logo in the JSON — and please make it raster, not SVG.
  3. "Verified connector" treatment + name delivery — deliver the name in the JSON, not mirrored in the fragment (fragment is untrusted; JSON comes from the trust-confirmed origin). The exact verified-vs-anonymous consent UI is ours to specify and I'll take it to the II design owners; the security contract we need from the server is: branding is shown ⇔ trust-confirmed origin + declared callback + valid slug + valid logo; otherwise fall back to the anonymous screen.
  4. Single doc vs. separate requests — single metadata document with the logo inlined as base64 data:. One request, one origin, no CSP friction.

Minor

  • Security section is solid: no client-supplied content, no SSRF in v1 (logos bundled at build time), closed slug space with 404 otherwise. If the Phase-3 open self-service path is ever built, the SSRF-pinned client + https-only + size/dimension caps + raster-only + decode-then-re-encode is exactly right — but the rendering hardening (size cap / raster / fail-closed) should be II-side and present from v1, not deferred.
  • Keeping native/desktop (loopback-redirect) connectors anonymous until a signed software_statement is the correct v1 cut — those are exempt from the callback allow-list, so there's no vetted-vendor anchor to derive identity from.

Bottom line: the trust model needs no changes. The one substantive revision is the logo transport — replace "II renders a cross-origin <img src>" with "II fetches from the trust-confirmed origin (or reads an inline base64 logo from the metadata JSON) and renders a data: raster image, failing closed to the anonymous screen." That's the only part that breaks on contact with II's CSP.


Generated by Claude Code

aterga added a commit that referenced this pull request Aug 18, 2026
Address the review pass on the scoping doc:

* Fix stale code citations to the current snapshot: get_info
  (tools.rs:1883), oql_needs_origin_error (1495), err (2287), the
  resource-not-found calls (2067/2078), and the skill:// resource
  generation loop (2033-2045).
* Replace the reference to a non-existent `scoping-client-branding.md`
  with the client-branding scoping proposal (PR #103).
* Correct the CIMD SSRF note: `markdown_url_for_base` is NOT a reusable
  SSRF guard (host-string compare only, ignores ports, no private-IP
  rejection; its 169.254 rejection is incidental to the fixed skills
  origin). A dedicated SSRF-safe fetcher is needed; the closest building
  block is discover.rs's ip_is_global.
* Drop `oql-schema://` from the public-cacheable resources: it is
  caller-gated (requires derivation_origin/account, runs under the user's
  delegated agent), so a public cache keyed only by canister ID could leak
  one identity's schema to another. Keep it a tool or an identity-keyed
  private resource.
* Fix the x-mcp-header section: there is no top-level `network` field or
  destructive-op discriminator to mirror into Mcp-Param-*; annotating
  absent fields would fail the mandated header/body validation. Use
  Mcp-Name for per-operation policy; scope any new param separately.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
aterga added a commit that referenced this pull request Sep 23, 2026
Implements the imcp2 side of the client-branding extension
(docs/scoping-client-branding.md, PR #103): let II show WHICH vetted
product is asking for a connect, instead of an anonymous "some bridge".

- New `branding` module: a server-curated table of vetted connectors
  (ChatGPT, Claude, Cursor, Grok, Perplexity, Google Antigravity), keyed on
  the request's already-validated `redirect_uri` vendor — never on the
  client-supplied `client_name`/`logo_uri`, which open DCR makes
  attacker-controlled (rendering them would be a consent-phishing gift). A
  connect that doesn't resolve to a vetted web vendor — including every
  loopback (native-app) redirect — keeps the status-quo anonymous screen.
- `authorize` resolves the connector for the validated redirect and passes
  its slug on the II connect link as `&connector=<slug>` (additive fragment
  param; an II that doesn't know it ignores it).
- Two issuer-rooted GET endpoints II fetches same-origin with the
  #4091-validated callback: `/branding/{slug}` (name + absolute logo URL +
  `verified`, `no-store`) and `/branding/{slug}/logo` (SVG, `nosniff`).
  Both 404 outside the curated set so the path can't probe.

The bundled logos are ORIGINAL neutral placeholders (a generic "verified
connector" mark), deliberately not reproductions of vendor logos — each
vendor's own licensed mark is dropped into its `Connector::logo` later.
Actually rendering this needs II's counterpart change (parse the slug,
fetch these endpoints, render); until then it is inert and harmless.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The first draft put the product slug in the connect-link fragment
(`&connector=<slug>`) and served an unbound `GET {issuer}/branding/{slug}`.
The fragment is written by whoever drives the browser, so that let a
consent screen vouch for a product the server never tied to the connect.

The contract now matches what #200 implements: II asks
`GET {issuer}/branding?state=` and the server answers from the connect's
validated redirect, with nothing about branding in the link. The revision
also:

- states precisely what branding vouches for (where the authorization code
  is delivered, not who started the connect or which vendor account ends up
  holding the grant) and the residual risks that follow;
- spells out the II side as numbered requirements: issuer derivation from
  the validated callback, response acceptance, one callback and one state
  for everything, an exact-origin trust list gating the whole treatment,
  and safe rendering (CSP, <img>, wording);
- notes that branding covers only compiled-in callbacks, never operator
  allow-list entries, and documents the logo hardening;
- replaces line-number citations with symbol references.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings September 24, 2026 15:04
@aterga aterga changed the title docs: scope client-branding extension (product name + logo to id.ai) docs: scope client branding (product name + logo to id.ai), session-bound contract Sep 24, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

The security-sensitive contract spans imcp2, a separate implementation PR, and future Internet Identity changes requiring human validation.

Review effort: Balanced
Findings: None

aterga added a commit that referenced this pull request Oct 2, 2026
CIMD shipped in #191 (opt-in) and #203 (on by default, with
OAUTH_CIMD_ENABLED as a kill switch), so the scoping plan now records
what was built instead of proposing it:

- the implemented gate, flow, fetcher, validation, caching, single-flight
  and in-flight bounds, configuration and rollback, by symbol rather than
  by line number;
- answers to the plan's open questions, and where the build departed from
  the plan: a domain-and-subdomain gate instead of exact origins, no cache
  floor or ETag revalidation, the same-origin redirect rule, discovery's
  SSRF guard tightened rather than left unchanged, and the rate cap
  dropped on purpose;
- branding moved to #103/#200 and keyed on the validated redirect. A
  client_id-domain key would have let any local program pose as a
  loopback CIMD client of a vetted vendor;
- the DoS residuals as built, the tests that pin the behaviour, and an
  index of where it lives.

Section numbers 3.4 and 3.5, which the code cites, keep their meaning.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants